Skip to main content

Effective Java 68条

img.png

1、可以用静态工厂方法替代构造函数

优点

  • 有名称,可读性高
  • 不需要在每次调用时都创建一个新对象
  • 可以返回其返回类型的任何子类型的对象

缺点

  • 如果仅提供静态工厂方法,则不能对没有公共或受保护构造函数的类进行子类实例化
  • 它们不容易与其他静态方法区分开来 如:valueOf、of、getInstance、newInstance、getType 和 newType

2、当有多构造函数时建议使用Builder模式

在设计其构造函数或静态工厂将具有多个参数的类时,这是一个不错的选择。

3、使用枚举类型创造单例

枚举 Singleton,防止反射破坏,这是实现单例的最佳方式。

4、使用私有构造函数来强制不可被实例化

对于只有静态函数,静态字段的类,建议私有化构造函数

5、避免创建对象

善于重用不可变的对象,避免多次创建

6、消除过时的对象引用

举个例子

    public class Stack{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack(){
elements = new Object [DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e){
ensureCapacity();
elements[size++] = e;
}

public Object pop(){
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}

private void ensureCapacity(){
if(elements.length == size)
elements = Array.copyOf(elements, 2 * size + 1);
}
}

这样写,pop出的对象并不会被垃圾回收收集,这种对象永远都不会被取消引用,很可怕 建议这样

public pop(){
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete references.
return result;
}

如果遇到不确定的对象生命周期时,可以考虑使用WeakHashMap缓存。

7、避免使用 finalizers

可预测的,通常是危险的,尽量避免使用。 该函数中的逻辑不能保证他们会被及时执行,且未捕获的异常甚至不会打印警告,会导致严重的性能损失 显式终止方法通常与try-finally构造结合使用以确保终止

    Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}

8、覆盖equals时请遵循

  • 自反性。 x.equals(x) == true
  • 对称性。 当且仅当y.equals(x)==true时,x.equals(y)==true
  • 传递性。 if(x.equals(y)&&y.equals(z)),y.equals(z)==true
  • 一致性。
  • 非空性。 x.equals(null)==false

永远不要忘记

  • 覆盖equals时始终覆盖hashCode

    每当在同一个对象中调用hashCode时,它应该返回相同的整数。 如果两个对象 equals 相等,则应该返回相同的hashCode。 不需要(但建议)两个不等于对象返回不同的hashCode。

  • 不要在equals声明中用其他类型替换Object

9、始终覆盖toString

Object的toString方法的通用约定是该对象的描述。注意覆盖时,如果有格式,请备注或者严格按照格式返回。

10、谨慎覆盖clone

建议提供对象复制的替代方法,或者根本不提供。如下

    public Yum(Yum yum);
    public static Yum newInstance(Yum yum);

11、考虑实现Comparable接口

对实现Comparable的对象数组进行排序非常简单如使用:Arrays.sort(a);

12、最小化类和成员的可访问性

目的是解耦,我的理解:不可被访问,意味着不可被修改,变量越小,稳定性越高。

  • 使每个类或成员尽可能不可访问
  • 实例字段永远不应该是公共的

13、在公有类中使用访问方法而非公有成员变量

    class Point {
public double x;
public double y;
}

这种是不可取的。

14、最小化可变性

实例的所有信息都是在创建时提供的。它们更易于设计、实施和使用。而且它们更不容易出错并且更安全

  • 不要提供任何修改对象状态的方法
  • 确保类不能扩展 使用final 修饰
  • 字段使用final
  • 将所有字段设为私有
  • 确保对任何可变组件的独占访问

例如

    public final class Complex {
private final double re;
private final double im;

public Complex (double re, double im) {
this.re = re;
this.im = im;
}

// Accessors with no corresponding mutators
public double realPart() { return re;}
public double imaginaryPart() { return im;}

public Complex add(Complex c){
return new Complex(re + c.re, im + c.im);
}

public Complex subtract(Complex c){
return new Complex(re - c.re, im - c.im);
}

...

@Override public boolean equals (Object o){...}
}

不可变对象很简单。他们一生只有一种状态。 不可变对象是线程安全的。不需要同步。它们可以自由共享,并且可以重用现有实例。 使用静态工厂可以创建经常请求实例的常量,并在以后的请求中为它们提供服务。 不可变对象的内部也可以共享。 它们为其他对象提供了很好的构建块。 缺点是不同的值需要单独的对象。在某些情况下,它可能会产生性能问题。

概括

  • 类应该是不可变的,除非有充分的理由使它们可变。
  • 如果一个类不能是不可变的,则尽可能限制它的可变性。
  • 除非有充分的理由不这样做,否则让每个属性都设计成final
  • 可以放宽一些规则以提高性能(缓存、延迟初始化......)

15、组合优于继承

继承有利于代码复用,但是尽可能不要进行跨包的继承。包内的继承是优秀的设计方式,一个包里的文件处在同一个程序员的控制之下。但是继承有其局限性:子类依赖于超类。超类一旦发生更改,将可能破坏子类。并且,如果超类是有缺陷的,子类也会得“遗传病”。

复合,即不扩展已有的类,而是在的类中新增一个现有类的。相当于现有类作为一个组建存在于新类中。如此,将只会用到需要用到的东西,而不表现现有类所有的方法和成员变量。新类也可以称为“包装类”,也就是设计模式中的Decorate模式。

16、除非真需要,否则就默认禁止继承

17、优于面向接口编程,而不是面向抽象

Java 只允许单一继承,接口通常是定义允许多种实现类型的最佳方式,现有的类可以很容易地改造以实现新的接口。接口支持安全、强大的功能增强。

18、接口只用于定义类型

应避免任何其他用途,例如常量接口如下:

    public interface Constants {
static final double AVOGRADOS_NUMBER = 6.02214199e23;
}

19、创造有层次的类结构而不是冗长的类

冗长的类

  • 可读性差
  • 增加内存占用
  • 容易出错且效率低下

// Tagged Class
class Figure{
enum Shaple {RECTANGLE, CIRCLE};

final Shape shape;

// Rectangle fields
double length;
double width;

//Circle field
double radius;

// Circle Constructor
Figure (double radius) {
shape = Shape.CIRCLE;
this.radius=radius;
}

// Rectangle Constructor
Figure (double length, double width) {
shape = Shape.RECTANGLE;
this.length=length;
this.width=width;
}

double area(){
switch(shape){
case RECTANGLE:
return length*width;
case CIRCLE
return Math.PI * (radius * radius);
defalult
throw new AssertionError();
}
}
}

有层次的类结构

  • 代码简单明了
  • 具体实现在它自己的类中
  • 所有字段都是final
  • 编译器确保每个类的构造函数初始化其数据字段
  • 可扩展性和灵活性

abstract class Figure{
abstract double area();
}
class Circle extends Figure{
final double radius;

Circle(double radius) { this.radius=radius;}

double area(){return Math.PI * (radius * radius);}
}
class Rectangle extends Figure{
final double length;
final double width;

Rectangle (double length, double width) {
this.length=length;
this.width=width;
}

double area(){return length*width;}
}

class Square extends Rectangle {
Square(double side){
super(side,side);
}
}

20、用函数对象表示策略

策略是允许程序存储和传输调用特定功能的能力的工具。类似于函数指针、委托或lambda 表达式 可以定义一个对象,其方法对其他对象执行操作


class StringLengthComparator{
public int compare(String s1, String s2){
return s1.length() - s2.length();
}
}

具体策略通常是无状态的,因此它们应该是单例的 为了能够传递不同的策略,客户端应该从策略接口而不是具体类调用方法,优化一下代码如下


public interface Comparator<T>{
public int compare(T t1, T t2);
}


实现


class StringLengthComparator implements Comparator<String>{
private StringLengthComparator(){} // Private constructor
public static final StringLengthComparator INSTANCE = new StringLengthComparator(); // Singleton instance
public int compare(String s1, String s2){
return s1.length() - s2.length();
}
}

21、优先考虑静态类成员

静态类相当于一个普通的外部类,不直接持有外部类对象的引用,也就不会影响垃圾回收机制。

22、不要在代码中使用原始类型

泛型类和接口是具有一个或多个类型参数作为泛型的类和接口 使用原始类型会失去泛型的安全性,如下

    
private final List stamps = ...

stamps.add(new Coin(...));

Stamp s = (Stamp) stamps.get(i);

无界通配符类型Set<?> 在需要泛型类型但我们不知道或不关心实际类型时使用。 永远不要将元素(null 除外)添加到Collection<?>。

23、消除未经检查的警告

如果不能在尽可能小的范围内使用Suppress-Warnings注释,请尽可能消除所有未经检查的警告。


Set<Lark> exaltation = new HashSet(); Warning, unchecked conversion found.
Set<Lark> exaltation = new HashSet<Lark>(); Good

24、列表优先于数组


// Fails at runtime
Object[] objectArray = new Long[1];
objectArray[0] ="I don't fit in" // Throws ArrayStoreException

// Won't compile
List<Object> ol = new ArrayList<Long>();//Incompatible types
ol.add("I don't fit in")

通过例子可以看出,数组只有在运行时才发现错误,而泛型可以在编译期就可以被发现。

25、推荐用泛型

26、推荐用泛型方法


//
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}

27、使用有界通配符来增加 API 的灵活性

未使用情况


public void pushAll(Iterable<E> src){
for(E e : src)
puhs(e)
}

// Integer is a subtype of Number
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...
numberStack.pushAll(integers); //Error message here: List<Integer> is not a subtype of List<Number>

使用子类通配符,协变,意味着某个继承自E的具体类型


public void pushAll(Iterable<? Extends E> src){
for (E e : src)
push(e);
}

使用超类通配符,逆变,意味着E类型的超类


public void pushAll(Iterable<? Extends E> src){
for (E e : src)
push(e);
}

28、考虑类型安全的异构容器



public class Favorites{
public void putFavorites(Class<T> type, T instance);
public <T> getFavorite(Class<T> type);
}



Favorites f = new Favorites();
f.putFavorites(String.class, "JAVA");
f.putFavorites(Integer.class, 0xcafecace);
f.putFavorites(Class.class, Favorite.class);

String s = f.getFavorites(String.class);
int i =f.getFavorites(Integer.class);
Class<?> c = f.getFavorites(Class.class);



public class Favorites{
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

public <T> void putFavorites(Class<T> type, T instance){
if(type == null)
throw new NullPointerException("Type is null");
favorites.put(type, type.cast(instance));//runtime safety with a dynamic cast
}

public <T> getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
}

相信上面的写法你们代码中肯定用过了。

29、用enum代替int常量


public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

有人会说Android中少用enum,但其实R8 编译优化时已经将枚举优化为整型,避免枚举造成的额外开销。

30、使用实例字段而不是序数



public enum Ensemble{
SOLO, DUET, TRIO...;
public int numberOfMusicians() {return ordinal() + 1}
}

更好的做法是



public enum Ensemble{
SOLO(1), DUET(2), TRIO(3)...TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {this.numberOfMusicians = size;}
public int numberOfMusicians() {return numberOfMusicians;}
}

31、用EnumSet代替位域


public class Text{
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

// 注意此处,使用的是Set而不是EnumSet
public void applyStyles(Set<Style> styles){
// ...
}
}

//
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

32、用EnumMap代替序数索引

任何时候都不要使用enum的ordinal()方法。

33、使用接口模拟可扩展枚举

枚举类型不能扩展另一个枚举类型



public interface Operation{
double apply(double x, double y);
}
public enum BasicOperation implements Operation{
PLUS("+"){
public double apply(double x, double y) {return x + y}
},
MINUS("-"){...},TIMES("*"){...},DIVIDE("/"){...};

private final String symbol;
BasicOperation(String symbol){
this.symbol = symbol;
}
@Override
public String toString(){ return symbol; }
}

BasicOperation是不可扩展的,但接口类型Operation是可扩展的

34、注解优先于命名模式

@Test注解的示例



//Marker annotation type declaration
import java.lang.annotation.*;

//Indicates that the annotated method is a test method.
//Use only on parameterless static methods.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

35、坚持使用Override注解

覆盖超类声明的每个方法声明上应使用Override注释,使用 Override 编译器会提醒我们更多信息。

36、检查参数的有效性

执行前检查参数,参数异常需要抛出Exception。私有方法利用断言assertion检查参数。在构造函数中也这样做。

37、必要时进行保护性拷贝

防御性地编程示例如下

  
//Broken "immutable" time period
public final class Period{
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start;
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
if(start.compare(end) > 0)
throw new IllegalArgumentException(start + " after " + end );
this.start = start;
this.end = end;
}

public Date start(){
return start;
}

public Date end(){
return end;
}
...
}

攻击


Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78)// Modifies internal of p!

防御


public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(start.compare(end) > 0)
throw new IllegalArgumentException(start + " after " + end );
}

它在检查参数和复制参数之间的时间内保护类免受另一个线程对参数的更改,从而实现安全性防御。

38、谨慎设计方法签名

  • 仔细选择方法名称
  • 避免长参数
  • 对于参数类型,优先使用接口而不是类
  • 与布尔参数相比,优先二元素枚举类型,如下

    public enum TemperatureScale {CELSIUS, FARENHEIT}

39、慎用重载

  • API 中不要有重载的方法,以免有混淆 API 的客户端。
  • 永远不要导出具有相同数量参数的重载。建议使用不同的名称。writeBoolean(boolean), writeInt(int), 和writeLong(long)
  • 对于构造函数,可以使用静态工厂

40、慎用可变参数

一句话:不应过度使用

41、返回0长度的数组或者集合,而不是null

42、为所有导出的API元素编写文档注释

要正确记录 API,必须在每个导出的类、接口、构造函数、方法和字段声明之前加上文档注释。 方法的文档注释应该简洁地描述,应包括如下:

  • 描述方法是做什么而不是它是如何工作的
  • @throws标签 描述前置条件
  • @return标签,即调用成功完成后的描述
  • 线程安全方面的描述
  • @param 元素的摘要描述

特别注意:

  • 泛型:记录所有类型参数
  • 枚举:记录所有常量、类型和公共方法
  • Annotatons:记录所有成员的类型

不要忘记记录

  • 线程安全级别
  • 序列化形式

43、最小化局部变量的范围

44、用for-each 循环而不是传统的 for 循环


// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
doSomething(e);
}

不能使用 for-each 循环的情况

  • 过滤——如果需要遍历一个集合并移除选中的元素,那么你需要使用一个显式的迭代器,这样你就可以调用它的 remove 方法。
  • 转换——如果需要遍历列表或数组并替换其元素的部分或全部值,那么需要列表迭代器或数组索引来设置元素的值。
  • 并行迭代——如果需要并行遍历多个集合,那么需要对迭代器或索引变量进行显式控制,以便所有迭代器或索引变量都可以同步推进。

45、了解和使用常用库

每个程序员都应该熟悉

  • java.lang
  • java.util
  • java.io
  • java.util.concurrent

46、如果需要准确的答案,请避免使用 float 和 double

对于货币计算,请使用int,对大于 18 位的数字使用BigDecimal

47、基本类型优先于装箱基本类型

48、如果有更精确的类型,请避免使用字符串

  • 字符串比其他类型更麻烦
  • 字符串不如其他类型灵活
  • 字符串比其他类型慢
  • 字符串比其他类型更容易出错
  • 字符串是其他值类型的不良替代品
  • 字符串是枚举类型的不良替代品
  • 字符串是聚合类型的不良替代品
  • 字符串是功能的不良替代品 所以,只使用 String 来表示文本

49、注意字符串连接的性能


// Inappropriate use of string concatenation - Performs horribly!
public String statement()
String result = "";
for (int i = 0; i < numItems(); i++)
result += lineForItem(i);
return result;

推荐使用


public String statement(){
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0; i < numItems(); i++)
b.append(lineForItem(i));
return b.toString();
}

高版本编译期会在编译成字节码时将 + 号优化为StringBuilder,所以用+还是StringBuilder,你可以看下当前jdk版本下编译后的字节码,如果已变StringBuilder,那就放心使用+

50、通过接口引用对象

如果存在适当的接口类型,则参数、返回值、变量和字段都应使用接口类型声明。


// Good - uses interface as type
List<Subscriber> subscribers = new Vector<Subscriber>();

51、接口优先于反射机制

为什么?

  • 失去编译时类型检查的所有好处
  • 执行反射访问的代码笨拙且冗长
  • 性能受到影响

作为一项规则,对象不应在运行时在正常应用程序中进行反射访问,反射的合法用途是管理一个类对运行时可能不存在的其他类、方法或字段的依赖关系。 反射在一些复杂的系统编程任务中非常强大和有用。它有很多缺点。如果可能,仅使用反射来实例化对象并使用编译时已知的接口或超类访问对象。

52、谨慎使用JNI

新的 Java 版本很少建议使用 NDK 来提高性能

53、谨慎进行优化

努力编写好程序而不是快速程序,速度会随之而来。 如果一个好的程序不够快,那就优化它的架构。

  • 过早优化是万恶之源
  • 在优化问题上遵循两条规则 (1)不要这样做 (2)暂时不要这样做——也就是说,在你有一个完全清晰且未经验证的解决方案之前不要这样做。

54、遵守普遍的命名规则

例如:

  • 包名:com.google.inject, org.joda.time.format
  • 类或接口:Timer, FutureTask, LinkedHashMap, HttpServlet
  • 方法或者属性:remove, ensureCapacity, getCrc
  • 常量:MIN_VALUE, NEGATIVE_INFINITY
  • 变量:i, xref, houseNumber
  • 泛型类型:T, E, K, V, X, T1, T2

55、只针对异常情况才使用异常

异常是针对特殊情况的。切勿对普通控制流使用或(在 API 中公开)异常。

56、对于可恢复的情况使用受检异常,对于编程错误的情况使用运行时异常

如果期望调用者适当的恢复,则需要使用受检异常,强迫调用者食用try-catch代码块,或者将他们抛出去,当调用发生前提违例——违反约定的情况时,使用运行时异常,这个时候程序已经无法再执行下去了。

57、避免不必要地异常检测

仅当这两种情况发生时才使用异常检查

  • 不能通过正确使用 API 来防止异常情况
  • 一旦遇到异常,使用 API 的程序员可以采取一些有用的措施。

将已检查的异常重构为未检查的异常,如下。


try {
obj.action(args);
} catch(TheCheckedException e) {
// Handle exceptional condition
...
}

重构为


if (obj.actionPermitted(args)) {
obj.action(args);
} else {
// Handle exceptional condition
...
}

58、使用通用异常

如:

  • IllegalArgumentException、非法参数异常
  • IllegalStateException、对象状态不适合方法调用
  • NullPointerException、空对象调用
  • IndexOutOfBoundsException、索引参数值超出范围,又称越界
  • ConcurrentModificationException、在禁止的地方检测到对象的并发修改
  • UnsupportedOperationException、对象不支持方法

59、记录每个方法抛出的所有异常

未经检查的异常通常代表编程错误,让程序员熟悉他们可能犯的所有错误有助于他们避免犯这些错误,始终单独声明已检查异常,并使用 Javadoc @throws 标记准确记录引发每个异常的条件。 在方法声明中不要使用 throws 关键字包含未经检查的异常。

60、抛异常时记录并包含故障详细信息

要捕获故障,异常的详细消息应包含导致异常的所有参数和字段的值,如:

    // Alternative IndexOutOfBoundsException.
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {...}

61、不要忽略异常

不要让 catch 块为空


// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}

62、同步访问共享可变数据

在 Java 中,除非类型为 long 或 double,否则读取或写入变量是原子的,但对于所有原子操作,它不能保证一个线程写入的值对另一个线程可见。线程之间的可靠通信以及互斥都需要同步。 一般来说:当多个线程共享可变数据时,每个读取或写入数据的线程都必须进行同步,最好的做法: 不共享可变数据。

63、避免过度同步

同步本身会带来一定的性能损耗,所以在使用同步时,应该考虑尽量少用同步,如果不得不用的情况,应该让同步的范围尽量小。

64、善于使用线程池,而不是直接使用线程

如:

ExecutorService executor = Executors.newSingleThreadExecutor();
//执行
executor.execute(runnable);
//释放
executor.shutdown();

对于负载较轻的应用程序,请使用:Executors.new-CachedThreadPool 对于负载较重的应用程序,请使用:Executors.newFixedThreadPool

65、善于并发库少用wait、notify等

  • 善用并发集合:如CopyOnWriteArrayList、阻塞队列BlockingQueue等
  • 善用同步器:CountDownLatch、Semaphore、CyclicBarrier、Exchanger等

wait:永远不要在循环之外调用它。循环用于测试等待前后的条件。


// The standard idiom for using the wait method
synchronized (obj) {
while (<condition does not hold>){
obj.wait(); // (Releases lock, and reacquires on wakeup)
}
... // Perform action appropriate to condition
}

notify:唤醒单个等待线程,假设存在这样的线程。 notifyAll:唤醒所有等待的线程。

66、给类、方法等注释线程安全标记

为了实现安全的并发使用,一个类、方法必须清楚地记录它支持的线程安全级别。

  • 不可变:不需要外部同步(即 String、Long、BigInteger)
  • 无条件线程安全:可变但具有内部同步。无需外部同步(即Random、ConcurrentHashMap)
  • 有条件的线程安全:某些方法需要外部同步。(即 Collections.synchronized 包装器)
  • 不是线程安全的:需要外部同步(即 ArrayList、HashMap)
  • 并发使用不安全(即 System.runFinalizersOnExit)

使用私有锁对象来防止用户在无条件线程安全的类中长时间持有锁。


// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();

public void foo() {
synchronized(lock) {
...
}
}

67、使用lazy初始化

如果仅在类的一小部分实例上访问字段并且初始化该字段的成本很高时使用。它降低了初始化类或创建实例的成本,但增加了访问它的成本。 多线程场景,应考虑是否允许被多次初始化。

68、不要依赖线程调度器

线程调度程序确定哪些可运行、可以运行以及运行多长时间。操作系统将尝试公平地做出此决定,但策略可能会有所不同。因此,任何依赖线程调度程序来确保正确性或性能的程序都可能是不可移植的。 好的做法就是,确保平均可运行线程数量不大于处理器数量。

参考文章

https://cloud.tencent.com/developer/article/1747625 https://github.com/HugoMatilla/Effective-JAVA-Summary#56-adhere-to-generally-accepted-naming-conventions